/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */

package org.mule.runtime.module.db.internal.domain.connection;

import org.mule.runtime.core.api.transaction.Transaction;
import org.mule.runtime.core.api.transaction.TransactionException;
import org.mule.runtime.module.db.internal.domain.transaction.DbTransactionManager;
import org.mule.runtime.module.db.internal.domain.transaction.TransactionalAction;
import org.mule.runtime.module.db.internal.domain.type.DbTypeManager;
import org.mule.runtime.module.db.internal.resolver.param.GenericParamTypeResolverFactory;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Creates connections using a {@link org.mule.runtime.module.db.internal.domain.transaction.DbTransactionManager} to track active
 * transactions.
 */
public class TransactionalDbConnectionFactory implements DbConnectionFactory {

  protected final Logger logger = LoggerFactory.getLogger(getClass());

  protected final DbTransactionManager dbTransactionManager;
  protected final DbTypeManager dbTypeManager;
  private final ConnectionFactory connectionFactory;
  private final DataSource dataSource;

  public TransactionalDbConnectionFactory(DbTransactionManager dbTransactionManager, DbTypeManager dbTypeManager,
                                          ConnectionFactory connectionFactory, DataSource dataSource) {
    this.dbTransactionManager = dbTransactionManager;
    this.dbTypeManager = dbTypeManager;
    this.connectionFactory = connectionFactory;
    this.dataSource = dataSource;
  }

  @Override
  public DbConnection createConnection(TransactionalAction transactionalAction) throws SQLException {
    Connection connection;

    try {
      connection = createDataSourceConnection(transactionalAction);
    } catch (ConnectionCreationException e) {
      throw new SQLException(e);
    }

    return doCreateDbConnection(connection, transactionalAction);
  }

  private Connection createDataSourceConnection(TransactionalAction transactionalAction) throws SQLException {
    Transaction tx = dbTransactionManager.getTransaction();
    Connection connection;


    if (transactionalAction == TransactionalAction.ALWAYS_JOIN) {
      if (tx == null) {
        throw new IllegalStateException("Transactional action is " + transactionalAction + " but there is no active transaction");
      } else {
        connection = getConnectionFromTransaction(tx, dataSource);
      }
    } else if (transactionalAction == TransactionalAction.JOIN_IF_POSSIBLE) {
      if (tx == null) {
        connection = connectionFactory.create(dataSource);
      } else {
        connection = getConnectionFromTransaction(tx, dataSource);
      }
    } else if (transactionalAction == TransactionalAction.NOT_SUPPORTED) {
      connection = connectionFactory.create(dataSource);
    } else {
      throw new IllegalArgumentException("There is no defined way to manage transactional action " + transactionalAction);
    }
    return connection;
  }

  protected DbConnection doCreateDbConnection(Connection connection, TransactionalAction transactionalAction) {
    return new DefaultDbConnection(connection, transactionalAction, new DefaultDbConnectionReleaser(this),
                                   new GenericParamTypeResolverFactory(dbTypeManager));
  }

  private Connection getConnectionFromTransaction(Transaction tx, DataSource dataSource) throws SQLException {
    Connection con;

    if (tx.hasResource(dataSource)) {
      if (logger.isDebugEnabled()) {
        logger.debug("Retrieving connection from current transaction: " + tx);
      }
      con = (Connection) tx.getResource(dataSource);
    } else {
      con = connectionFactory.create(dataSource);

      try {
        tx.bindResource(dataSource, con);
      } catch (TransactionException e) {
        if (con != null && !con.isClosed()) {
          con.close();
        }

        throw new ConnectionBindingException("Could not bind connection to current transaction: " + tx, e);
      }
    }

    return con;
  }

  @Override
  public void releaseConnection(DbConnection connection) {
    if (connection == null) {
      return;
    }

    try {
      if (connection.isClosed()) {
        return;
      }
    } catch (SQLException e) {
      if (logger.isDebugEnabled()) {
        logger.debug("Error checking for closed connection on releasing connection", e);
      }
      return;
    }
    final Transaction transaction = dbTransactionManager.getTransaction();
    boolean closeConnection = false;
    if (connection.getTransactionalAction() == TransactionalAction.NOT_SUPPORTED) {
      closeConnection = true;
    } else if (connection.getTransactionalAction() == TransactionalAction.JOIN_IF_POSSIBLE && transaction == null) {
      closeConnection = true;
    }

    if (closeConnection) {
      RuntimeException exception = null;
      try {
        if (!connection.getAutoCommit()) {
          connection.commit();
        }
      } catch (SQLException e) {
        exception = new ConnectionCommitException(e);
      } finally {
        try {
          connection.close();
        } catch (SQLException e) {
          if (exception == null) {
            exception = new ConnectionClosingException(e);
          }
        }
      }
      if (exception != null) {
        throw exception;
      }
    }
  }
}
